package com.googlecode.ddlutilstool.gui;

import java.awt.BorderLayout;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;

import javax.swing.JInternalFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;

import org.jedit.syntax.HTMLTokenMarker;
import org.jedit.syntax.JEditTextArea;
import org.jedit.syntax.JavaScriptTokenMarker;
import org.jedit.syntax.PropsTokenMarker;
import org.jedit.syntax.TSQLTokenMarker;
import org.jedit.syntax.TextAreaDefaults;
import org.jedit.syntax.TokenMarker;
import org.jedit.syntax.XMLTokenMarker;

/**
 * <p>The EditorPanel is the text editor
 * used in each window of the EditorDesktop.
 * It is really a wrapper for the JEditTextArea
 * field, a text field that formats text based
 * on the file type.</p>
 * 
 * <p>The JEditTextArea field was written for
 * JEdit, and released to open source.  It uses
 * italics and bold to format XML and SQL.</p>
 * 
 * <p>This panel also contains the UndoManager,
 * in charge of performing Undo and Redo, and
 * holding the current state.  The parent
 * EditorDesktop uses each EditorPanel's UndoManager
 * to update the menu, and to execute the Undo and
 * Redo commands.</p>
 */
@SuppressWarnings("serial")
public class EditorPanel extends JPanel  implements UndoableEditListener, DocumentListener {

	private EditorDesktop mDesktop;
	private JInternalFrame mParent;
    protected UndoManager undo = new UndoManager();
    
    private String mTitle;
    private JTextField mTitlePanel;
	private JEditTextArea mTextField;

	private boolean mDirty = false;
	private File mFile;
	
	public EditorPanel(EditorDesktop pDesktop, JInternalFrame pParent) {
		super();
		mDesktop = pDesktop;
		mParent = pParent;
		
		setLayout(new BorderLayout());
		
	    mTitle = "";
		mTitlePanel = new JTextField(mTitle);
		mTitlePanel.setEditable(false);
		add(mTitlePanel,BorderLayout.NORTH);
		
		TextAreaDefaults defaults = TextAreaDefaults.getDefaults();
		defaults.eolMarkers=false;
		defaults.inputHandler.addKeyBinding("C+z", mDesktop.getUndoAction());
		defaults.inputHandler.addKeyBinding("C+y", mDesktop.getRedoAction());
		defaults.inputHandler.addKeyBinding("C+c", mDesktop.getCopyAction());
		defaults.inputHandler.addKeyBinding("C+x", mDesktop.getCutAction());
		defaults.inputHandler.addKeyBinding("C+v", mDesktop.getPasteAction());
		defaults.inputHandler.addKeyBinding("C+s", mDesktop.getSaveAction());
		defaults.inputHandler.addKeyBinding("C+o", mDesktop.getOpenAction());
		defaults.inputHandler.addKeyBinding("C+l", mDesktop.getCloseAction());
		
		mTextField = new JEditTextArea(defaults);
		mTextField.getDocument().addUndoableEditListener(this);
		mTextField.getDocument().addDocumentListener(this);
		mTextField.createDefaultPopup();
		
		add(mTextField,BorderLayout.CENTER);
	}
	
	/**
	 * Sets the parent object, in this case it will be
	 * the EditorDesktop's InternalPane that holds
	 * the current panel.
	 * @param pParent
	 */
	public void setParent(JInternalFrame pParent) {
		mParent = pParent;
	}
	
	/**
	 * The current window title, which will contain
	 * the file name.
	 * @return the window title for display
	 */
	public String getTitle() {
		return mTitle;
	}
	
	/**
	 * Sets the window title for display.  This
	 * will update the window header.
	 * 
	 * @param pTitle the current window's filename
	 */
	public void setTitle(String pTitle) {
		mTitle = pTitle;
		mParent.setTitle(pTitle);
	}
	
	/**
	 * @return the file associated with the window contents, if any.
	 */
	public File getFile() {
		return mFile;
	}
	
	/**
	 * Sets the current window as enabled or
	 * disabled.  It is used during DDL execution
	 * to freeze the UI, the unfreeze it.
	 */
	public void setEnabled(boolean pEnabled) {
		super.setEnabled(pEnabled);
		mTextField.setEnabled(pEnabled);
	}
	
	/**
	 * True if the file has been altered
	 * @return
	 */
	public boolean isDirty() {
		return mDirty;
	}
	
	/**
	 * @return true if there is only whitespace or nothing at all in the text area.
	 */
	public boolean isEmpty() {
		String content = mTextField.getText();
		if (content!=null && content.trim().length()>0) {
			return false;
		}
		else {
			return true;
		}
	}
	
	/**
	 * Forces the dirty flag.
	 * @param pForce
	 */
	private void updateDirty(boolean pForce) {
		mDirty = pForce;
		propagateDirty();
	}
	
	/**
	 * Sets the dirty flag using the UndoManager.
	 * This should work, because if the file was saved,
	 * the undo manager is cleared.
	 */
	public void updateDirty() {
		mDirty = undo.canUndo();
		propagateDirty();
	}
	
	/**
	 * Checks the dirty flag, and makes sure a star is there
	 * if the file is dirty.
	 */
	private void propagateDirty() {
		if (mDirty) {
			mTitlePanel.setText("*"+mTitle);
		}
		else {
			mTitlePanel.setText(mTitle);
		}
	}

    /**
     * @return true if there are edits to be undone
     */
	public boolean canUndo() {
		return undo.canUndo();
	}

	/**
	 * Undoes the appropriate edits.  
	 */
	public void undo() {
		undo.undo();
	}
	
	/**
	 * 
	 * @return a description of the undoable form of this edit.
	 */
	public String getUndoPresentationName() {
		return undo.getUndoPresentationName();
	}
	
	/**
	 * @return true if edits may be redone.
	 */
	public boolean canRedo() {
		return undo.canRedo();
	}
	
	/**
	 * Undoes the appropriate edits.  
	 */
	public void redo() {
		undo.redo();
	}
	
	/**
	 * @return a description of the redoable form of this edit.
	 */
	public String getRedoPresentationName() {
		return undo.getRedoPresentationName();
	}
	
	/**
	 * Places the selected text into the clipboard.
	 */
	protected void copy() {
		mTextField.copy();
	}
	
	/**
	 * Deletes the selected text from the text area and places it
	 * into the clipboard.
	 */
	protected void cut() {
		mTextField.cut();
	}
	
	/**
	 * Inserts the clipboard contents into the text.
	 */
	protected void paste() {
		mTextField.paste();
	}
	
	/**
	 * Called by the Desktop when the user selects the menu
	 * options Save or Save As, it updates the window
	 * with the correct filename, changes the formatting
	 * if required.  It will save the contents
	 * to the file, clear the undo manager's queue,
	 * and turn off the dirty flag.
	 * 
	 * @param pFile
	 * @return true if the file was saved.
	 * @throws IOException
	 */
	public boolean saveInternal(File pFile) throws IOException {
		mFile = pFile;
		setTitle(mFile.getName());
		if (saveInternal()) {
			mTextField.setTokenMarker(getTokenMarker(pFile.getName()));
			mTextField.invalidate();
			return true;
		}
		else {
			return false;
		}
	}
	
	/**
	 * Called by the Desktop when the user selects the menu
	 * options Save or Save As, it updates the window
	 * with the correct filename, changes the formatting
	 * if required.  It will save the contents
	 * to the file, clear the undo manager's queue,
	 * and turn off the dirty flag.
	 * 
	 * @return true if the file was saved.
	 * @throws IOException
	 */
	public boolean saveInternal() throws IOException {
		FileWriter writer = null;
		try {
			mTextField.setEditable(false);
			writer = new FileWriter(mFile);
			writer.write(mTextField.getText());
			writer.flush();
			undo.discardAllEdits();
			mDesktop.updateActions();
			updateDirty();
			return true;
		}
		finally {
			mTextField.setEditable(true);
			if (writer!=null) try {
				writer.close();
			}
			catch (IOException e) {}
		}
	}
	
	/**
	 * Loads in the current file into the editor.
	 * @param pFile
	 * @throws IOException
	 */
	public void load(File pFile) throws IOException {
		mFile = pFile;
		setTitle(pFile.getName());
		StringWriter writer = new StringWriter();
		PrintWriter out = new PrintWriter(writer);
		
		FileReader reader = null;
		try {
			reader = new FileReader(pFile);
			BufferedReader in = new BufferedReader(reader);
			String line = in.readLine();
			while (line!=null) {
				out.print(line);
				out.print("\n");
				line = in.readLine();
			}
			out.flush();
			
		}
		finally {
			if (reader!=null) reader.close();
		}
		forceText(writer.toString(),pFile.getName());
		updateDirty(false);
	}

	/**
	 * Used internally to set the contents of the text area.
	 * It updates the title, the formatting and the content.
	 * It also clears the undo queue, updates the menus
	 * and scrolls to the top of the content.
	 * 
	 * @param pContent The file contents
	 * @param pName the filename
	 */
	protected void forceText(String pContent, String pName) {
		mTextField.setTokenMarker(getTokenMarker(pName));
		mTextField.setText(pContent);
		setTitle(pName);
		undo.discardAllEdits();
		mDesktop.updateActions();
		updateDirty(true);
 		mTextField.scrollTo(0, 0);
	}
	
	/**
	 * @return a Reader containing the contents of the editor.
	 */
	protected Reader getReader() {
		return new StringReader(mTextField.getText());
	}
	
	/**
	 * Called when the window is closed, it clears the content.
	 */
	public void forceClose() {
		mTitle="";
		mTextField.setText("");
		mFile=null;
		undo.discardAllEdits();
		mDesktop.updateActions();
		updateDirty();
	}
	
	
	/**
	 * Undo actions
	 * Override
	 * @see javax.swing.event.UndoableEditListener#undoableEditHappened(javax.swing.event.UndoableEditEvent)
	 */
	public void undoableEditHappened(UndoableEditEvent e) {
		undo.addEdit(e.getEdit());
		mDesktop.updateActions();
	}

	/**
	 * Called because this class implements DocumentListener,
	 * it updates the dirty flag to true.
	 */
    public void changedUpdate(DocumentEvent e) {
    	if (!mDirty) updateDirty(true);
	}

	/**
	 * Called because this class implements DocumentListener,
	 * it updates the dirty flag to true.
	 */
	public void insertUpdate(DocumentEvent e) {
    	if (!mDirty) updateDirty(true);
 	}

	/**
	 * Called because this class implements DocumentListener,
	 * it updates the dirty flag to true.
	 */
	public void removeUpdate(DocumentEvent e) {
    	if (!mDirty) updateDirty(true);
 	}

	/**
	 * Matches the file to the syntax marker.
	 * 
	 * @param pFile
	 * @return
	 */
	protected TokenMarker getTokenMarker(File pFile) {
		return getTokenMarker(pFile.getName());
	}
	
	/**
	 * Matches the file to the syntax marker.
	 * 
	 * @param pFile
	 * @return
	 */
	protected TokenMarker getTokenMarker(String pFilename) {
		TokenMarker marker = null;
		String filename = pFilename.toLowerCase();
		if (filename.endsWith("properties")) {
			marker = new PropsTokenMarker();
		}
		else if (filename.endsWith("html")) {
			marker = new HTMLTokenMarker();
		}
		else if (filename.endsWith("xml")) {
			marker = new XMLTokenMarker();
		}
		else if (filename.endsWith("js")) {
			marker = new JavaScriptTokenMarker();
		}
		else if (filename.endsWith("sql")) {
			marker = new TSQLTokenMarker();
		}
		return marker;
	}
	
	/**
	 * <p>Class DatabaseFileFilter</p>
	 * <p>A file filter for the JFileChooser, it accepts
	 * only sql and xml files.</p>
	 */
	static class DatabaseFileFilter extends FileFilter {
		public String getDescription() {
			return "Database Files (*.xml, *.sql)";
		}
		public boolean accept(File f) {
			return f.isDirectory() || (f.isFile() && (f.getName().toLowerCase().endsWith(".xml") || f.getName().toLowerCase().endsWith(".sql")));
		}
	}
}
